/***********************************************************************
 * BinTrotter : BitTorrent library/client for the Android operating system.
 *  
 * Copyright (C) 2009, Christopher David Stones (chris.stones@gmail.com)
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 ***********************************************************************/


package org.bintrotter.apeer;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

import org.bintrotter.ClientInfo;
import org.bintrotter.Main;
import org.bintrotter.apeer.receivestates.ReceiveState;
import org.bintrotter.apeer.receivestates.States;

import org.bintrotter.afiles.Files;
import org.bintrotter.afiles.Hasher;
import org.bintrotter.contentmanager.ContentManager;
import org.bintrotter.metafile.Metafile;
import org.bintrotter.stats.Stats;

public class Peer {

	public BlockTransit	incommingBlocks;
	private BlockTransit	outgoingBlocks;
	
	private boolean			m_remoteChoke;
	private boolean			m_remoteInterested;
	private boolean			m_localChoke;
	private boolean			m_localInterested;
	private Stats			m_stats;
	private long			m_lastUpdatedInterestedStatus;
	
	private State   		m_state;
	private SocketChannel 	m_channel;
	public SelectionKey   m_selectionKey;
	private long			m_timeCreated;
	private StateRunner		m_stateRunner;
	private Files			m_files;
	
	private ContentManager m_RemoteContent;
	
	public LinkedList<Integer>	outHaveQueue;
	public LinkedList<Request>	outCancelQueue;
	public LinkedList<Request>	outPieceQueue;
	
	private boolean m_shouldSendUnchoke;
	private boolean m_shouldSendChoke;
	private boolean m_shouldSendInterested;
	private boolean m_shouldSendNotInterested;
	
	public enum State {
		
		CONNECTING,
		HANDSHAKE,
		PROTOCOL,
		SHUTDOWN,
		DEAD;
	}

	public Peer(SocketAddress sad, Files files, Stats stats, Selector selector) {
		
		try {
			m_stats 			= stats;
			m_RemoteContent 	= null;
			m_localChoke 		= true;
			m_remoteChoke 		= true;
			m_state				= State.CONNECTING;
			
			m_files				= files;
			m_timeCreated		= System.currentTimeMillis();
			 
			outHaveQueue 		= new LinkedList<Integer>();
			outCancelQueue		= new LinkedList<Request>();
			outPieceQueue		= new LinkedList<Request>();
			m_stateRunner		= new StateRunner(this);
		
			m_lastUpdatedInterestedStatus = 0;
			
			m_channel = SocketChannel.open();
			m_channel.configureBlocking(false);
			m_channel.connect(sad);
			m_selectionKey = m_channel.register(selector, SelectionKey.OP_CONNECT, this);

		} catch (IOException e) {

			m_state	= State.DEAD;
		}		
	}
	
	private void updateInterestedStatus() {
		
		long now = (new Date()).getTime(); 
		if( now - m_lastUpdatedInterestedStatus > 30000)
			m_lastUpdatedInterestedStatus = now;
		else
			return;
			
		
		boolean interested = m_files.getHavePieceMask().intersted( m_RemoteContent );
		
		if(interested) {
			
			m_shouldSendNotInterested = false;
			if(m_localInterested == false) {
				m_shouldSendInterested= true;
				
			}
			
		} else {
			
			m_shouldSendInterested = false;
			if(m_localInterested == true)
				m_shouldSendNotInterested= true;
		}
	}
	
	/*
	 * Update which piece this peer should work on.
	 */
	private void updateRequests() {
		
		if(m_RemoteContent != null && m_localInterested && (m_remoteChoke == false)) {
			if(incommingBlocks != null && incommingBlocks.queued() == 0 && incommingBlocks.inTransit() == 0) {
			
				try {

					if(Hasher.SHA1(incommingBlocks.piece, m_files)) {
						
						m_files.getHavePieceMask().set(incommingBlocks.piece);
						Main.log.info("RECEIVED WHOLE PIECE " + incommingBlocks.piece );
						incommingBlocks = null;
					}
					else {
						m_files.getHavePieceMask().unset(incommingBlocks.piece);
						Main.log.info("CORRUPT PIECE" + incommingBlocks.piece);
						incommingBlocks = new BlockTransit( incommingBlocks.piece, m_files);
					}
					
				} catch( Exception e) {
					Main.log.error("cant hash piece " + incommingBlocks.piece);
					incommingBlocks = null;
				}
			}
			
			if(incommingBlocks==null)
			{
				int nextPiece = (int)m_files.getHavePieceMask().randomNeededCodeFrom(m_RemoteContent);
				
				if(nextPiece>=0) {
					incommingBlocks = new BlockTransit(nextPiece, m_files);
					Main.log.info("starting piece " + incommingBlocks.piece);
				}
				else {
					m_shouldSendNotInterested = true;
					incommingBlocks = null;
				}
			}
		}
	}
	
	public State update() {
	
		updateInterestedStatus();
		updateRequests();
		
		m_state = m_stateRunner.update();
			
		return (m_state);
	}
	
	public long timeCreated() {

		return m_timeCreated;
	}

	public SocketChannel channel() {

		return m_channel;
	}

	public Metafile metafile() {
		
		return m_files.metafile;
	}

	public ReceiveState get(States s) {
		
		return m_stateRunner.get(s);
	}
	
	public MappedByteBuffer[] mmap(Request rq) throws IOException {
		
 		
		return m_files.mmap(rq.index, rq.begin, rq.length);
	}

	/// FUNCTIONS CALLED BY INCOMMING DATA HANDLERS
		public void inChoke(boolean b) {
			
			m_remoteChoke = b;
			
			// FIXME: need to set cancel outgoing requests, set BlockState as UNASSIGNED
			// FIXME: also, need to track pending blocks !!!!!!
			// FIXME: need to add methods for signaling cancel messages to idle state !!!
			//if(m_remoteChoke) {
				//outRequestQueue.clear();
			//}
		}
	
		public void inInterested(boolean b) {
			
			m_remoteInterested = b;
		}
	
		public void inBitfield(byte[] buffer) throws Exception {

			int size = m_files.metafile.pieces();
			 
			m_RemoteContent = new ContentManager(16); // FIXME: 2^x==size;
			
			for(int p=0;p<size;p++)
				if((buffer[p/8] & (0x80>>(p%8))) != 0)
					m_RemoteContent.set(p);
		}
		
		public void inHave(int m_piece) throws Exception {

	//		int size = m_files.metafile.pieces();
			
			if(m_RemoteContent == null)
				m_RemoteContent = new ContentManager(16); // FIXME: 2^x==size;
				
			m_RemoteContent.set(m_piece);
		}
	
		public void inRequest(int index, int begin, int length) {
			
			/*** TODO: ***/
		}
	
		public void inDHTPort(int port) {
			// TODO handle DHT port message
			
		}
		
		/***
		 * Called when a block is received from the remote peer.
		 * @param piece piece that the block belongs to.
		 * @param begin offset of this block within the given piece
		 * @param length size of this block in bytes
		 */
		public void inBlockComplete(int piece, int begin, int length) {
			
			m_stats.dataIn(length);
			
			this.incommingBlocks.onComplete( piece, begin,length );
		}
		
		public void inCancel(int index, int begin, int length) {

			for(Iterator<Request> it = outPieceQueue.iterator(); it.hasNext(); ) {
				
				Request rq = it.next();
				
				if(rq.begin == begin && rq.index == index && rq.length == rq.length) {
					it.remove();
					return;
				}
			}
		}
		
	/// FUNCTIONS CALLED BY OUTGOING DATA HANDLERS
		
		public byte[] outInfoHash() {
			
			return m_files.metafile.infoHash.SHA1Bytes;
		}

		public byte[] outPeerID() {

			return ClientInfo.instance().peer_id.getBytes();
		}

		public int outPort() {

			return 0;
		}

		public boolean outShouldSendInterested() {
			
			if(m_shouldSendInterested) {
				
				m_localInterested = true;
				m_shouldSendInterested = false;
				return true;
			}
			return false;
		}

		public boolean outShouldSendNotInterested() {
			
			if(m_shouldSendNotInterested) {
				
				m_localInterested = false;
				m_shouldSendNotInterested = false;
				return true;
			}
			return false;
		}

		public boolean outShouldSendUnchoke() {
			
			if(m_shouldSendUnchoke) {
				m_localChoke = false;
				m_shouldSendUnchoke = false;
				return true;
			}
			return false;
		}

		public boolean outShouldSendChoke() {
			
			if(m_shouldSendChoke) {
				m_localChoke = true;
				m_shouldSendChoke = false;
				
				outPieceQueue.clear();
				
				return true;
			}
			return false;
		}

		public byte[] outBitField() {
			
//			TODO:
	
			return null;
		}
}

